package com.gulimall.seckill.service.impl;

import com.alibaba.csp.sentinel.annotation.SentinelResource;
import com.alibaba.csp.sentinel.slots.block.BlockException;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.TypeReference;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.common.to.MemberTo;
import com.common.to.Seckill.SeckillSessionTo;
import com.common.to.Seckill.SeckillSkuRedisTo;
import com.common.to.Seckill.SeckillSkuRelation;
import com.common.to.SkuInfoTo;
import com.common.to.mq.SeckillOrderTo;
import com.common.utils.R;
import com.gulimall.seckill.Interceptor.SeckillInterceptor;
import com.gulimall.seckill.feign.CouponFeignService;
import com.gulimall.seckill.feign.ProductFeignService;
import com.gulimall.seckill.service.SeckillService;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RSemaphore;
import org.redisson.api.RedissonClient;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.BeanUtils;
import org.springframework.data.redis.core.BoundHashOperations;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;

import javax.annotation.Resource;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 * @Author Rookie-6688
 * @Description
 * @Create 2021-05-13 9:47
 */
@Slf4j
@Service
public class SeckillServiceImpl implements SeckillService {

    private final String SESSION__CACHE_PREFIX = "seckill:sessions:"; // 秒杀场次前缀
    private final String SECKILL_CHARE_PREFIX = "seckill:skus";     // 商品消息前缀
    private final String SKU_STOCK_SEMAPHORE = "seckill:stock:";    // 商品库存前缀
    private final String SKU_BUY_PREFIX = "seckill:buy:";    // 秒杀当前场次商品唯一性确定前缀

    @Resource
    private CouponFeignService couponFeignService;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private ProductFeignService productFeignService;
    @Resource
    private RedissonClient redissonClient;
    @Resource
    private RabbitTemplate rabbitTemplate;

    /**
     * 将近三天的秒杀商品添加到redis中
     */
    @Override
    public void uploadSeckillSkuLasted3Days() {
        // 1、获取近三天所有优惠场次以及商品关系数据
        R r = couponFeignService.getLasted3DaysList();
        if(r.getCode() == 0){
            List<SeckillSessionTo> seckillSessionToList = r.getData(new TypeReference<List<SeckillSessionTo>>() {});
            if(!CollectionUtils.isEmpty(seckillSessionToList)){

                // 2、将秒杀活动的场次信息存储到redis
                for (SeckillSessionTo seckillSessionTo : seckillSessionToList) {
                    String key = SESSION__CACHE_PREFIX + seckillSessionTo.getStartTime().getTime() + "-" + seckillSessionTo.getEndTime().getTime();
                    // 如果存在数据，则说明重复操作，跳过（保证幂等性）
                    if (!stringRedisTemplate.hasKey(key)) {
                        List<String> skuIdList = seckillSessionTo.getRelationSkus().stream().map(seckillSkuRelation -> seckillSessionTo.getId() + "-" + seckillSkuRelation.getSkuId().toString()).collect(Collectors.toList());
                        stringRedisTemplate.opsForList().leftPushAll(key, skuIdList);
                    }


                    // 3、将秒杀活动的商品信息存储到 redis
                    SeckillSkuRedisTo seckillSkuRedisTo = new SeckillSkuRedisTo();
                    for (SeckillSkuRelation relationSkus : seckillSessionTo.getRelationSkus()) {
                        BoundHashOperations<String, Object, Object> seckillHashOps = stringRedisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                        String productInfoKey = seckillSessionTo.getId() + "-" + relationSkus.getSkuId();
                        // 在不存在数据时才添加数据
                        if (!seckillHashOps.hasKey(productInfoKey)) {
                            // 3.1 设置基本属性
                            R r1 = productFeignService.info(relationSkus.getSkuId());
                            if (r1.getCode() == 0) {
                                SkuInfoTo skuInfo = r1.getData("skuInfo", new TypeReference<SkuInfoTo>() {
                                });
                                seckillSkuRedisTo.setSkuInfo(skuInfo);
                            }
                            // 3.2 设置秒杀信息
                            BeanUtils.copyProperties(relationSkus, seckillSkuRedisTo);

                            // 3.3 设置秒杀时间信息
                            seckillSkuRedisTo.setStartTime(seckillSessionTo.getStartTime().getTime());
                            seckillSkuRedisTo.setEndTime(seckillSessionTo.getEndTime().getTime());

                            //3.4 设置商品的随机码（防止恶意攻击）
                            String token = UUID.randomUUID().toString().replace("-", "");
                            seckillSkuRedisTo.setRandomCode(token);

                            // 存入redis中
                            String s = JSON.toJSONString(seckillSkuRedisTo);
                            seckillHashOps.put(productInfoKey, s);

                            // 4 向redis设置信号量，模拟商品库存
                            RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + token);
                            // 设置信号量
                            semaphore.trySetPermits(seckillSkuRedisTo.getSeckillCount());
                        }
                    }
                }
            }
        }
    }

    public List<SeckillSkuRedisTo> blockHandler(BlockException e){
        log.info("执行了限流方法。。。。。。。。。。。。。。。");
        return null;
    }

    @SentinelResource(value = "resourceName1",blockHandler = "blockHandler")
    @Override
    public List<SeckillSkuRedisTo> getCurrentSeckillSkus() {
        // 1、先查看当前是属于哪一场的秒杀
        long currentTime = System.currentTimeMillis();
        Set<String> keys = stringRedisTemplate.keys(SESSION__CACHE_PREFIX + "*");   // 获取所有符合的key集合
        if (keys != null) {
            for (String key : keys) {
                String replace = key.replace(SESSION__CACHE_PREFIX, "");
                // 计算开始和结束时间
                String[] s = replace.split("-");
                long startTime = Long.parseLong(s[0]);
                long endTime = Long.parseLong(s[1]);
                if(currentTime>=startTime && currentTime<= endTime){
                    List<String> keyList = stringRedisTemplate.opsForList().range(key, 0,-1 );  // 库存场次里所有场次ID_商品ID集合
                    if(!CollectionUtils.isEmpty(keyList)){
                        // 得到所有商品秒杀消息
                        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
                        List<String> seckillProductList = hashOps.multiGet(keyList);//
                        if (seckillProductList != null) {
                            return seckillProductList.stream().map(seckillProduct -> JSON.parseObject(seckillProduct.toString(), SeckillSkuRedisTo.class)).collect(Collectors.toList());
                        }
                    }
                }
            }
        }
        return null;
    }

    @Override
    public SeckillSkuRedisTo getSkuSeckillInfo(Long skuId) {
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);

        // 1、获取所有当前sku的key
        Set<String> keys = hashOps.keys();
        SeckillSkuRedisTo seckillSkuRedisTo = null;
        if (keys != null) {
            for (String key : keys) {
                String reg = "\\d-" + skuId;    // 正则表达式
                if(Pattern.matches(reg, key)){
                    // 2、如果匹配，继续获取其对应的 value 值
                    seckillSkuRedisTo = JSON.parseObject(hashOps.get(key), SeckillSkuRedisTo.class);
                    // 3、获取其开始、结束时间，寻找在其中的，如果没有当前场次的，那么就将秒杀随机码设为null，返回
                    long currentTimeMillis = System.currentTimeMillis();
                    Long startTime = seckillSkuRedisTo.getStartTime();
                    Long endTime = seckillSkuRedisTo.getEndTime();
                    if(currentTimeMillis >= startTime && currentTimeMillis <= endTime){
                        // 如果在当前场次，那么直接返回
                        return seckillSkuRedisTo;
                    }
                    // 如果这个场次不存在，那么就检查下一个
                }
            }
            // 执行到这里说明改商品没有参与当前场的秒杀，那么就将随机码设为null
            if(seckillSkuRedisTo != null){
                seckillSkuRedisTo.setRandomCode(null);
            }
        }
        return null;
    }

    @Override
    public String seckill(String killId, String key, Integer num){
        // 1、获取对应秒杀商品相关的数据
        BoundHashOperations<String, String, String> hashOps = stringRedisTemplate.boundHashOps(SECKILL_CHARE_PREFIX);
        String s = hashOps.get(killId);
        SeckillSkuRedisTo seckillSkuRedisTo = JSON.parseObject(s, SeckillSkuRedisTo.class);

        // 2、检验秒杀随机码是否一致
        String randomCode = null;
        if (seckillSkuRedisTo != null) {
            randomCode = seckillSkuRedisTo.getRandomCode();
            if(randomCode.equals(key)){
                // 3、校验秒杀时间
                Long startTime = seckillSkuRedisTo.getStartTime();
                Long endTime = seckillSkuRedisTo.getEndTime();
                long currentTime = System.currentTimeMillis();
                if(currentTime >= startTime && currentTime <= endTime){
                    // 4、校验购买数量限制是否超过
                    if(num <= seckillSkuRedisTo.getSeckillLimit()){
                        // 5、校验场次消息以及sku是否一致（Redis存储数据的正确性）
                        String key1 = seckillSkuRedisTo.getPromotionSessionId() + "-" + seckillSkuRedisTo.getSkuInfo().getSkuId();
                        if(key1.equals(killId)){
                            // 6、最后校验当前是否是第一次秒杀这个商品，如果是就存入redis
                            // 设置 userID+场次ID+skuId
                            MemberTo memberTo = SeckillInterceptor.userMap.get();
                            String seckillKey = SKU_BUY_PREFIX + memberTo.getId() + "_" + key;
                            Boolean ifAbsent = stringRedisTemplate.opsForValue().setIfAbsent(seckillKey, num.toString(), endTime - currentTime, TimeUnit.MILLISECONDS);
                            if(ifAbsent){
                                // 当前是第一次秒杀当前场次的当前商品
                                // 7、进行信号量的消减
                                RSemaphore semaphore = redissonClient.getSemaphore(SKU_STOCK_SEMAPHORE + key);
                                boolean b = false;
                                try {
                                    b = semaphore.tryAcquire(num, 100, TimeUnit.MILLISECONDS);
                                    // 8、将消息发送给消息队列，在后台进行下单任务对数据库的操作（库存扣减、订单添加等操作）
                                    if(b){
                                        String orderSn = IdWorker.getTimeId();
                                        SeckillOrderTo seckillOrderTo = new SeckillOrderTo();
                                        seckillOrderTo.setOrderSn(orderSn);
                                        seckillOrderTo.setNum(num);
                                        seckillOrderTo.setMemberId(memberTo.getId());
                                        seckillOrderTo.setPromotionSessionId(seckillSkuRedisTo.getPromotionSessionId());
                                        seckillOrderTo.setSeckillPrice(seckillSkuRedisTo.getSeckillPrice());
                                        seckillOrderTo.setSkuId(seckillSkuRedisTo.getSkuId());
                                        rabbitTemplate.convertAndSend("order-event-exchange","order.seckill.order",seckillOrderTo);
                                        return orderSn;
                                    }
                                } catch (InterruptedException e) {
                                    e.printStackTrace();
                                }
                            }

                        }
                    }
                }
            }
        }
        return null;
    }
}
